//  Sample Delphi 3 program to rotate a 24-bit color bitmap any angle.
//
//  A 640 x 480 pixel x 24-bit color bitmap can be rotated any angle
//  in about a second on a 166 MHz Pentium.
//
//  Copyright (C) 1997-1998 Earl F. Glynn, Overland Park, KS  USA.
//  All Rights Reserved.

unit ScreenRotateScanLine;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtDlgs, StdCtrls, ExtCtrls, Spin;

type
  TFormRotateScanLine = class(TForm)
    ImageOriginal: TImage;
    ButtonLoad: TButton;
    LabelImageInfo: TLabel;
    ImageRotated: TImage;
    LabelRotateTime: TLabel;
    SpinEditThetaDegrees: TSpinEdit;
    LabelDegreesClockwise: TLabel;
    LabelRotationAngle: TLabel;
    LabelCenter: TLabel;
    SpinEditI: TSpinEdit;
    SpinEditJ: TSpinEdit;
    LabelI: TLabel;
    LabelJ: TLabel;
    CheckBoxStretch: TCheckBox;
    SpinEditThetaDegreesHundredths: TSpinEdit;
    Label1: TLabel;
    OpenPictureDialog: TOpenPictureDialog;
    ButtonSaveRotated: TButton;
    SavePictureDialog: TSavePictureDialog;
    procedure ButtonLoadClick(Sender: TObject);
    procedure SpinEditRotate(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure CheckBoxStretchClick(Sender: TObject);
    procedure ButtonSaveRotatedClick(Sender: TObject);

  private
    { Private declarations }
    BitmapOriginal:  TBitmap;
    BitmapRotated :  TBitmap;
  public
    PROCEDURE RotateBitmap;
  end;

var
  FormRotateScanLine: TFormRotateScanLine;

implementation
{$R *.DFM}

USES
{$IFDEF GIF}
  GIFImage,                    // TGIFImage
{$ENDIF}
  GraphicsConversionsLibrary,  // LoadGraphicsFile
  IniFiles,                    // TIniFile
  ImageLib;

CONST
  KeywordSetup     = 'Setup';
  KeywordDirectory = 'Directory';


// Load 24-bit color image from disk
procedure TFormRotateScanLine.ButtonLoadClick(Sender: TObject);
  VAR
    Filename:  STRING;
    NewPath :  STRING;
    IniFile :  TIniFile;
begin
  IF   OpenPictureDialog.Execute
  THEN BEGIN
    BitmapOriginal.Free;
    BitmapOriginal := LoadGraphicsFile(OpenPictureDialog.Filename);

    IF   BitmapOriginal.PixelFormat <> pf24bit
    THEN BitmapOriginal.PixelFormat := pf24bit;   // force to 24 bits

    // Update INI file for next time
    Filename := ChangeFileExt(ParamStr(0), '.INI');
    NewPath := ExtractFilePath(OpenPictureDialog.Filename);
    OpenPictureDialog.InitialDir := NewPath;
    IniFile := TIniFile.Create(Filename);
    TRY
      Inifile.WriteString(KeywordSetup, KeywordDirectory, NewPath)
    FINALLY
      IniFile.Free
    END;

    // Flush INI cache
    WritePrivateProfileString(NIL, NIL, NIL, pChar(Filename));

    

    ImageOriginal.Picture.Graphic := BitmapOriginal;
    LabelImageInfo.Caption := IntToStr(BitmapOriginal.Width) + ' by ' +
                              IntToStr(BitmapOriginal.Height) + ' pixels:  ' +
                              OpenPictureDialog.Filename;

    SpinEditI.Value := BitmapOriginal.Width  DIV 2;
    SpinEditJ.Value := BitmapOriginal.Height DIV 2;

    // Rotate and display the image
    RotateBitmap
  END
end;


// Rotate image by angle[degrees] specified in spinbox
procedure TFormRotateScanLine.SpinEditRotate(Sender: TObject);
begin
  RotateBitmap
end;


procedure TFormRotateScanLine.FormCreate(Sender: TObject);
  VAR
    IniFile :  TIniFile;
begin
{$IFDEF GIF}
  OpenPictureDialog.Filter := OpenPictureDialog.Filter +
                              '|GIFs|*.GIF';
{$ENDIF}

  IniFile := TIniFile.Create(ChangeFileExt(ParamStr(0), '.INI'));
  TRY
    OpenPictureDialog.InitialDir := Inifile.ReadString(KeywordSetup,
                                                       KeywordDirectory,
                             ExtractFilePath(ParamStr(0)))
  FINALLY
    IniFile.Free
  END;

  BitmapOriginal := TBitmap.Create;
  BitmapRotated  := TBitmap.Create;
end;


procedure TFormRotateScanLine.FormDestroy(Sender: TObject);
begin
  BitmapOriginal.Free;
  BitmapRotated.Free
end;


PROCEDURE TFormRotateScanLine.RotateBitmap;
  VAR
    cosTheta       :  DOUBLE;
    Delta          :  DWORD;     // D3/D4 compatibility
    i              :  INTEGER;
    iRotationAxis  :  INTEGER;
    iOriginal      :  INTEGER;
    iPrime         :  INTEGER;
    iPrimeRotated  :  INTEGER;
    j              :  INTEGER;
    jRotationAxis  :  INTEGER;
    jOriginal      :  INTEGER;
    jPrime         :  INTEGER;
    jPrimeRotated  :  INTEGER;
    RowOriginal    :  pRGBArray;
    RowRotated     :  pRGBArray;
    sinTheta       :  DOUBLE;
    StartTime      :  DWORD;
    Theta          :  DOUBLE;       // radians
begin
  // The size of BitmapRotated is the same as BitmapOriginal.  PixelFormat
  // must also match since 24-bit GBR triplets are assumed in ScanLine.
  BitmapRotated.Width  := BitmapOriginal.Width;
  BitmapRotated.Height := BitmapOriginal.Height;
  BitmapRotated.PixelFormat := pf24bit;

  // "Start" the clock
  StartTime := GetTickCount;

  // Axis of rotation is normally center of image
  TRY
    iRotationAxis := SpinEditI.Value
  EXCEPT
    iRotationAxis := 0
  END;

  TRY
    jRotationAxis := SpinEditJ.Value
  EXCEPT
    jRotationAxis := 0
  END;

  // Convert degrees to radians.  Use minus sign to force clockwise rotation.
  Theta := -(SpinEditThetaDegrees.Value +
             SpinEditThetaDegreesHundredths.Value/100) *
           PI / 180;
  sinTheta := SIN(Theta);
  cosTheta := COS(Theta);

  // Step through each row of rotated image.
  FOR j := BitmapRotated.Height-1 DOWNTO 0 DO
  BEGIN
    RowRotated  := BitmapRotated.Scanline[j];

    // Assume the bitmap has an even number of pixels in both dimensions and
    // the axis of rotation is to be the exact middle of the image -- so this
    // axis of rotation is not at the middle of any pixel.

    // The transformation (i,j) to (iPrime, jPrime) puts the center of each
    // pixel at odd-numbered coordinates.  The left and right sides of each
    // pixel (as well as the top and bottom) then have even-numbered coordinates.

    // The point (iRotationAxis, jRotationAxis) identifies the axis of rotation.

    // For a 640 x 480 pixel image, the center point is (320, 240).  Pixels
    // numbered (index i) 0..319 are left of this point along the "X" axis and
    // pixels numbered 320..639 are right of this point.  Likewise, vertically
    // pixels are numbered (index j) 0..239 above the axis of rotation and
    // 240..479 below the axis of rotation.

    // The subtraction (i, j) - (iRotationAxis, jRotationAxis) moves the axis of
    // rotation from (i, j) to (iRotationAxis, jRotationAxis), which is the
    // center of the bitmap in this implementation.

    jPrime := 2*(j - jRotationAxis) + 1;

    FOR i := BitmapRotated.Width-1 DOWNTO 0 DO
    BEGIN

      iPrime := 2*(i - iRotationAxis) + 1;

      // Rotate (iPrime, jPrime) to location of desired pixel
      // Note:  There is negligible difference between floating point and
      // scaled integer arithmetic here, so keep the math simple (and readable).
      iPrimeRotated := ROUND(iPrime * CosTheta - jPrime * sinTheta);
      jPrimeRotated := ROUND(iPrime * sinTheta + jPrime * cosTheta);

      // Transform back to pixel coordinates of image, including translation
      // of origin from axis of rotation to origin of image.
      iOriginal := (iPrimeRotated - 1) DIV 2 + iRotationAxis;
      jOriginal := (jPrimeRotated - 1) DIV 2 + jRotationAxis;

      // Make sure (iOriginal, jOriginal) is in BitmapOriginal.  If not,
      // assign blue color to corner points.
      IF   (iOriginal >= 0) AND (iOriginal <= BitmapOriginal.Width-1) AND
           (jOriginal >= 0) AND (jOriginal <= BitmapOriginal.Height-1)
      THEN BEGIN
        // Assign pixel from rotated space to current pixel in BitmapRotated
        RowOriginal := BitmapOriginal.Scanline[jOriginal];
        RowRotated[i]  := RowOriginal[iOriginal]
      END
      ELSE BEGIN
        RowRotated[i].rgbtBlue  := 255;  // assign "corner" color
        RowRotated[i].rgbtGreen := 0;
        RowRotated[i].rgbtRed   := 0
      END

    END
  END;
  Delta := GetTickCount - StartTime;    // "stop" the clock
  LabelRotateTime.Caption := 'Rotation Time = ' + IntToStr(Delta) + ' ms';

  ImageRotated.Picture.Graphic := BitmapRotated

END {RotateImage};


procedure TFormRotateScanLine.CheckBoxStretchClick(Sender: TObject);
begin
  ImageOriginal.Stretch := CheckBoxStretch.Checked;
  ImageRotated.Stretch := CheckBoxStretch.Checked
end;

procedure TFormRotateScanLine.ButtonSaveRotatedClick(Sender: TObject);
begin
  IF   SavePictureDialog.Execute
  THEN BitmapRotated.SaveToFile(SavePictureDialog.Filename)
end;

end.
